El objetivo general de la evaluación es aprender a implementar los modelos de Data Science explicados en el módulo 5. Para este fin hemos escogido una base de datos del año 2017 para predecir el total del precio de casas en el mercado de Pekín (totalPrice) a través de diferentes métodos y algoritmos con el objetivo de seleccionar aquel o aquellos métodos que sean más óptimos.
Variables de la base de datos
Lng : coordenada de longitud utilizando el protocolo BD09
Lat: coordenada de latitud utilizando el protocolo BD09
DOM: días activos en el mercado. Más información en https://en.wikipedia.org/wiki/Days_on_market
followers: el número de personas que siguen la transacción.
square: número total de metros.
livingRoom: número de habitaciones.
drawingRoom: número de salones.
kitchen: número de cocinas.
bathroom: número de baños.
floor: el número total de pisos en el edificio.
buildingType: (tower) torre (1), bungalow (2), (combination of plate and tower), combinación de lámina y torre (3), (plate) lámina (4)
renovationCondition: Otros (1), ásperas (2), sencillez (3), duras (4)
buildingStructure: desconocida (1), mixta (2), ladrillo y madera (3), ladrillo y cemento (4), acero (5) acero y hormigón (6).
ladderRatio: Describe cuántas escaleras tiene un residente en promedio.
elevator: tiene ascensor (1) no tiene ascensor (0)
fiveYearsProperty: si el dueño tiene la propiedad por menos de 5 años (1) en caso contrario (0)
subway: si está cerca el metro (1) en caso contario (0)
price: precio por metro cuadrado en yuanes.
totalPrice: precio total en millones de yuanes.
Tareas encomendadas
Preprocesado de la información:
Dada la transcendencia de esta primera fase del análisis y de la cantidad de tiempo y recursos que conlleva (entre el 70 y el 80 por ciento del trabajo real) se considera muy importante realizar a fondo esta tarea antes de aplicar los modelos.
Resúmenes de la información a través de tablas y análisis gráficos.
Análisis de correlación.
Estudio de valores faltantes y valores extremos.
Selección de variables.
Otros tratamientos que se consideren oportunos.
Especifique y justifique cómo utiliza la muestra para la estimación de los modelos.
Métodos básicos para la REGRESIÓN:
Árboles de decisión. (CART y Random Forest)
Métodos de vecindad (K-vecinos)
Redes Neuronales. (Perceptron Multicapa)
Máquinas de vectores soporte.
Bagging y Boosting.
Gradient Boosting.
Explicabilidad del modelo elegido como óptimo.
Se valorará que se incluyan otros modelos y algoritmos que se consideren oportunos. Por ejemplo: Regresión Lineal, Regresión Lasso, y/o Redes neuronales de Base Radial, etcétera.
En esta primera fase preparamos el entorno de trabajo cargando las librerías necesarias. Para ello, comprobamos previamente si están instaladas en el sistema. A continuación, cargamos los datos del mercado inmobiliario de Pekín del año 2017, que utilizaremos para desarrollar y evaluar distintos modelos de predicción.
# Instalación de paquetes solo si es necesario
if (!require("tidyverse")) install.packages("tidyverse")
if (!require("readr")) install.packages("readr")
if (!require("janitor")) install.packages("janitor")
if (!require("skimr")) install.packages("skimr")
if (!require("caret")) install.packages("caret")
if (!require("GGally")) install.packages("GGally")
if (!require("corrplot")) install.packages("corrplot")
if (!require("rpart")) install.packages("rpart")
if (!require("randomForest")) install.packages("randomForest")
if (!require("class")) install.packages("class")
if (!require("nnet")) install.packages("nnet")
if (!require("e1071")) install.packages("e1071")
if (!require("gbm")) install.packages("gbm")
if (!require("xgboost")) install.packages("xgboost")
if (!require("DALEX")) install.packages("DALEX")
if (!require("DALEXtra")) install.packages("DALEXtra")
if (!require("forcats")) install.packages("forcats")
if (!require("recipes")) install.packages("recipes")
if (!require("tidymodels")) install.packages("tidymodels")
if (!require("data.table")) install.packages("data.table")
if (!require("kableExtra")) install.packages("kableExtra")
if (!require("magrittr")) install.packages("magrittr")
if (!require("inspectdf")) install.packages("inspectdf")
if (!require("reactable")) install.packages("reactable")
if (!require("ggmap")) install.packages("ggmap")
if (!require("ggdensity")) install.packages("ggdensity")
if (!require("geomtextpath")) install.packages("geomtextpath")
# Carga de librerías
library(tidyverse)
library(readr)
library(janitor)
library(skimr)
library(caret)
library(GGally)
library(corrplot)
library(rpart)
library(randomForest)
library(class)
library(nnet)
library(e1071)
library(gbm)
library(xgboost)
library(DALEX)
library(DALEXtra)
library(forcats)
library(recipes)
library(tidymodels)
library(data.table)
library(kableExtra)
library(magrittr) # Para usar %<>%
library(inspectdf)
library(reactable)
library(ggmap)
library(ggdensity)
library(geomtextpath)Leemos el fichero original con los datos del mercado inmobiliario de Pekín del año 2017. A continuación, realizamos una limpieza básica: eliminamos posibles filas duplicadas y transformamos algunas variables numéricas en variables categóricas, ya que representan información cualitativa. Posteriormente, mostramos las primeras observaciones.
# Leer el conjunto de datos original
datOri <- fread('/Users/oscar/Desktop/BIG DATA/2o TRIMESTRE/MODULO 5_Mineria de datos 1/3_Autoevaluacion/M05_AutoEv_Oscar Porta/mercado_beijing_2017.csv') %>%
as.data.table()
# Eliminar filas duplicadas
datOri %<>%
distinct() %>%
as.data.table()
# Conversión de variables numéricas a factores
datOri %<>%
mutate( buildingType = as.factor(buildingType)) %>%
mutate( renovationCondition = as.factor(renovationCondition)) %>%
mutate( buildingStructure = as.factor(buildingStructure)) %>%
mutate( fiveYearsProperty = as.factor(fiveYearsProperty)) %>%
mutate( subway = as.factor(subway)) %>%
mutate( elevator = as.factor(elevator)) %>%
as.data.table()
# Visualización interactiva de las primeras observaciones con estilo reactable
reactable(head(datOri, 10),
bordered = TRUE,
highlight = TRUE,
striped = TRUE,
pagination = FALSE,
class = "reactable-table")Realizamos un análisis automático del conjunto de datos utilizando funciones de la librería inspectdf. Esto nos permitirá entender mejor la estructura, distribución y calidad de los datos antes de realizar cualquier modelado. Representamos las correlaciones más fuertes, los desequilibrios de categorías, el uso de memoria, la presencia de valores ausentes y las distribuciones de variables numéricas.
# Correlación entre columnas numéricas (solo las mayores o iguales a 0.5 en valor absoluto)
x <- inspect_cor(datOri)
x <- x %>% dplyr::filter(abs(corr) >= 0.5 | corr <= -0.5) %>% as_tibble()
show_plot(x)De esta primera iteración en el EDA, obtenemos las siguientes conclusiones:
Variables categóricas:
La mayoría de variables presentan la cardinalidad esperada según la descripción original del dataset.
Frecuencia mayoritaria en variables categóricas:
buildingStructure: 6 (71%)
elevator: 1 (70%)
subway: 1 (60%)
fiveYearsProperty: 1 (56%)
renovationCondition: 1 (46%)
buildingType: 4 (38%)
Datos faltantes (porcentaje):
Correlación de variables (top 5):
livingRoom - square: 0.743
bathRoom - square: 0.716
square - totalPrice: 0.644
drawingRoom - square: 0.580
Variables numéricas destacadas:
DOM presenta valores atípicos por encima de 1000, cuando el 75% está por debajo de 200.
followers tiene valores extremos por encima de 600, con una mediana de 3.
ladderRatio tiene una mediana de 0.33 pero llega hasta valores de 10.
price y square presentan distribuciones asimétricas, concentradas en valores bajos y colas largas a la derecha.
A partir de este análisis, tomamos las siguientes decisiones:
Imputaremos buildingType, que presenta un porcentaje reducido de valores perdidos.
Aunque algunas variables numéricas presentan outliers, por ahora solo tomamos nota para decidir más adelante posibles transformaciones.
Recodificamos los niveles de las variables categóricas para facilitar su interpretación en gráficos y modelos.
# Recodificar buildingType
datOri$buildingType <- factor(datOri$buildingType,
labels = c("torre", "bungalow", "combinacion", "lamina"))
# Recodificar renovationCondition
datOri$renovationCondition <- factor(datOri$renovationCondition,
labels = c("otros", "asperas", "sencillez", "duras"))
# Recodificar buildingStructure
datOri$buildingStructure <- factor(datOri$buildingStructure,
labels = c("desconocida", "mixta", "ladrillo y madera",
"ladrillo y cemento", "acero", "acero y hormigon"))
# Recodificar elevator
datOri$elevator <- factor(datOri$elevator,
labels = c("No", "Si"))
# Recodificar fiveYearsProperty
datOri$fiveYearsProperty <- factor(datOri$fiveYearsProperty,
labels = c("No", "Si"))
# Recodificar subway
datOri$subway <- factor(datOri$subway,
labels = c("No", "Si"))
# Volver a graficar composición de variables categóricas
x <- inspect_cat(datOri)
show_plot(x)Antes de proceder a la modelización, realizamos una visualización espacial básica para observar la distribución geográfica de diferentes variables del dataset. En concreto, representamos la localización de los inmuebles según varias variables categóricas y el precio. Esto nos permite detectar si existe una posible concentración geográfica asociada a ciertas categorías, lo cual puede ser relevante en el análisis predictivo posterior.
# Visualización geográfica sin mapa base (pero manteniendo lat/lon)
# Función general para graficar distribución geográfica por variable
mapa_simple_por_variable <- function(var) {
ggplot(datOri, aes(x = Lng, y = Lat, color = .data[[var]])) +
geom_point(alpha = 0.6, size = 1.3) +
facet_wrap(as.formula(paste("~", var))) +
ggtitle(paste("Distribución espacial según:", var)) +
theme_minimal() +
theme(axis.title = element_blank())
}
# Mapas categóricos
mapa_simple_por_variable("buildingType")# Mapa continuo de price
ggplot(datOri, aes(x = Lng, y = Lat, color = price)) +
geom_point(size = 1.3, alpha = 0.9) +
ggtitle("Distribución espacial de Price") +
theme_minimal() +
theme(axis.title = element_blank())
Del análisis espacial realizado a partir de las coordenadas de longitud
y latitud, podemos extraer las siguientes observaciones clave para cada
una de las variables representadas:
Observamos que los edificios del tipo “torre” se concentran principalmente en el centro de la ciudad, mientras que en las zonas periféricas predomina el tipo “lámina”. Además, detectamos un pequeño núcleo urbano con presencia destacada de edificios tipo “bungalow”, lo que podría corresponder a un casco antiguo o una zona residencial tradicional.
Las viviendas cercanas a estaciones de metro se concentran en el centro de Pekín. En la periferia, la proporción de propiedades cerca del metro disminuye notablemente, lo que puede estar reflejando una menor densidad de infraestructuras de transporte público en esas zonas.
Las propiedades con ascensor se encuentran principalmente en el área central de la ciudad. En cambio, en los barrios periféricos predominan las propiedades sin ascensor, lo cual podría relacionarse con la antigüedad de las construcciones o con edificios de menor altura.
En el centro de la ciudad se concentran las estructuras más modernas, construidas en acero y hormigón. En cambio, hacia la periferia predominan las construcciones mixtas o de ladrillo y cemento. También se identifica un punto donde se concentran edificaciones de ladrillo y madera, lo que podría indicar una zona de valor histórico o residencial tradicional.
No se detecta un patrón espacial claro en cuanto al estado de renovación de las propiedades. Los distintos niveles de esta variable se encuentran distribuidos de forma bastante homogénea a lo largo del mapa.
Los precios por metro cuadrado tienden a ser más elevados en el centro urbano, donde además la densidad de viviendas es mayor. A medida que nos alejamos del centro hacia las zonas periféricas, los precios disminuyen, lo que refleja una clara correlación espacial entre localización y valor del inmueble.
Antes de construir modelos con transformaciones más cuidadas, decidimos ejecutar un primer modelo baseline usando la función h2o.automl(), que nos permite probar múltiples algoritmos automáticamente y obtener información valiosa como:
El tipo de modelo con mejor ajuste.
Un valor de referencia del error del modelo (baseline).
La importancia relativa de las variables.
En primer lugar, inicializamos el entorno de H2O para poder lanzar modelos de machine learning sobre nuestra base de datos. Esta operación se realiza solo una vez por sesión y debe ejecutarse de forma interactiva, no al generar el documento HTML.
Este bloque debe ejecutarse manualmente cada vez que se abra RStudio, pero no se ejecuta al knitear para evitar errores de conexión con H2O.
A continuación, ejecutamos el primer modelo baseline con H2O AutoML. Este proceso entrena múltiples modelos automáticamente durante 5 minutos, usando validación cruzada y dejando fuera GLM y StackedEnsemble para centrarnos en métodos más complejos. Este bloque se ejecuta exclusivamente en RStudio y se desactiva al generar el HTML (eval=FALSE) para evitar errores o entrenamientos innecesarios.
# Convertir a formato H2O
data <- as.h2o(datOri)
# Dividir el dataset (train, valid, test)
splits <- h2o.splitFrame(
data = data,
ratios = c(0.7, 0.15),
destination_frames = c("train", "valid", "test"),
seed = 1
)
train <- splits[[1]]
valid <- splits[[2]]
test <- splits[[3]]
# Definir variables
y <- "totalPrice"
x <- setdiff(names(data), y)
# Ejecutar H2O AutoML (5 minutos, sin GLM ni StackedEnsemble)
aml <- h2o.automl(
x = x,
y = y,
training_frame = train,
validation_frame = valid,
nfold = 3,
max_runtime_secs = 300,
stopping_metric = "RMSE",
exclude_algos = c("GLM", "StackedEnsemble"),
stopping_tolerance = 0.1,
stopping_rounds = 5,
seed = 12345,
sort_metric = "RMSE"
)Una vez finaliza la ejecución del AutoML, guardamos los resultados más relevantes: el modelo líder, su tabla de rendimiento, el gráfico de importancia de variables y el error RMSE. Esto nos permite volver a usarlos más adelante sin reentrenar.
# Guardar leaderboard, modelo, RMSE y variable importancia
best_mod <- aml@leader
lb_df <- as.data.frame(aml@leaderboard)
var_import <- h2o.varimp(best_mod)
aml_perf <- h2o.performance(model = best_mod, newdata = test)
err_val_pre <- h2o.rmse(aml_perf)
# Crear carpeta si no existe
if (!dir.exists("resultados")) dir.create("resultados")
# Guardar todo
h2o.saveModel(best_mod, path = "resultados", force = TRUE)
saveRDS(lb_df, file = "resultados/lb_df.rds")
saveRDS(var_import, file = "resultados/var_import.rds")
saveRDS(err_val_pre, file = "resultados/err_val_pre.rds")
saveRDS(best_mod, file = "resultados/best_mod.rds")Para mostrar los resultados en el informe sin volver a ejecutar el modelo, cargamos los objetos guardados en el paso anterior. Este bloque sí se ejecuta al knit.
# Cargar leaderboard, RMSE y variable importance guardados
lb_df <- readRDS("resultados/lb_df.rds")
var_import <- readRDS("resultados/var_import.rds")
err_val_pre <- readRDS("resultados/err_val_pre.rds")
best_mod <- readRDS("resultados/best_mod.rds") Mostramos a continuación los resultados más relevantes del AutoML: los 10 mejores y 10 peores modelos, el gráfico de importancia de variables y el RMSE obtenido en test. Este bloque sí se ejecuta al knit.
# Top-10 mejores modelos
lb_df %>% head(10) %>%
kbl(caption = "Top-10 Mejores Modelos Ejecutados - Ordenados por RMSE") %>%
kable_minimal()| model_id | rmse | mse | mae | rmsle | mean_residual_deviance |
|---|---|---|---|---|---|
| GBM_grid_1_AutoML_1_20250603_84523_model_112 | 40.63040 | 1650.830 | 8.095330 | 0.1039192 | 1650.830 |
| GBM_grid_1_AutoML_1_20250603_84523_model_165 | 41.26995 | 1703.208 | 7.626729 | NA | 1703.208 |
| GBM_grid_1_AutoML_1_20250603_84523_model_9 | 41.79594 | 1746.900 | 11.603605 | NA | 1746.900 |
| DeepLearning_grid_1_AutoML_1_20250603_84523_model_1 | 42.06689 | 1769.623 | 17.125697 | NA | 1769.623 |
| DeepLearning_grid_1_AutoML_1_20250603_84523_model_2 | 43.17752 | 1864.298 | 17.165955 | NA | 1864.298 |
| GBM_grid_1_AutoML_1_20250603_84523_model_252 | 43.70957 | 1910.526 | 11.025980 | NA | 1910.526 |
| GBM_grid_1_AutoML_1_20250603_84523_model_150 | 44.15492 | 1949.657 | 19.201593 | NA | 1949.657 |
| GBM_grid_1_AutoML_1_20250603_84523_model_170 | 44.31392 | 1963.723 | 9.774251 | NA | 1963.723 |
| GBM_grid_1_AutoML_1_20250603_84523_model_52 | 44.66802 | 1995.232 | 9.240851 | 0.1066327 | 1995.232 |
| GBM_grid_1_AutoML_1_20250603_84523_model_68 | 45.24717 | 2047.306 | 15.756929 | NA | 2047.306 |
# Top-10 peores modelos
lb_df %>% tail(10) %>%
kbl(caption = "Top-10 Peores Modelos Ejecutados - Ordenados por RMSE") %>%
kable_minimal()| model_id | rmse | mse | mae | rmsle | mean_residual_deviance | |
|---|---|---|---|---|---|---|
| 275 | GBM_grid_1_AutoML_1_20250603_84523_model_136 | 108.7975 | 11836.89 | 42.69203 | 0.2523016 | 11836.89 |
| 276 | GBM_grid_1_AutoML_1_20250603_84523_model_11 | 108.8203 | 11841.86 | 43.50324 | 0.2504584 | 11841.86 |
| 277 | GBM_grid_1_AutoML_1_20250603_84523_model_89 | 109.7195 | 12038.36 | 40.84170 | NA | 12038.36 |
| 278 | GBM_grid_1_AutoML_1_20250603_84523_model_100 | 110.7469 | 12264.88 | 47.28652 | 0.2563844 | 12264.88 |
| 279 | GBM_grid_1_AutoML_1_20250603_84523_model_233 | 111.1368 | 12351.38 | 42.45046 | 0.2553346 | 12351.38 |
| 280 | GBM_grid_1_AutoML_1_20250603_84523_model_39 | 112.5641 | 12670.68 | 60.59995 | NA | 12670.68 |
| 281 | DeepLearning_grid_1_AutoML_1_20250603_84523_model_4 | 113.5529 | 12894.26 | 41.24660 | NA | 12894.26 |
| 282 | GBM_grid_1_AutoML_1_20250603_84523_model_234 | 115.9740 | 13449.97 | 51.25668 | 0.2734822 | 13449.97 |
| 283 | GBM_grid_1_AutoML_1_20250603_84523_model_172 | 125.0143 | 15628.59 | 56.31988 | 0.2874309 | 15628.59 |
| 284 | GBM_grid_1_AutoML_1_20250603_84523_model_94 | 127.2491 | 16192.34 | 58.92600 | 0.2941695 | 16192.34 |
# Gráfico de variables importantes
ggplot(var_import[1:10, ], aes(x = fct_reorder(variable, scaled_importance), y = scaled_importance)) +
geom_col(fill = 'darkgreen') +
coord_flip() +
labs(
title = "VARIABLES IMPORTANTES",
subtitle = "Top-10",
y = 'Importancia Escalada',
x = "Variable"
) +
theme_bw() +
theme(
plot.title = element_text(size = 14, face = "bold", colour = "black"),
axis.title.x = element_text(size = 14, face = "bold", colour = "black"),
axis.title.y = element_text(size = 14, face = "bold", colour = "black"),
axis.text.x = element_text(size = 12, face = "bold", colour = "black"),
axis.text.y = element_text(size = 12, face = "bold", colour = "black")
)## [1] 74.94872
## Model Details:
## ==============
##
## H2ORegressionModel: gbm
## Model ID: GBM_grid_1_AutoML_1_20250603_84523_model_112
## Model Summary:
## number_of_trees number_of_internal_trees model_size_in_bytes min_depth
## 1 71 71 1253678 13
## max_depth mean_depth min_leaves max_leaves mean_leaves
## 1 13 13.00000 180 2264 1402.70420
##
##
## H2ORegressionMetrics: gbm
## ** Reported on training data. **
##
## MSE: 0.5027646
## RMSE: 0.7090589
## MAE: 0.2773273
## RMSLE: 0.008203069
## Mean Residual Deviance : 0.5027646
##
##
## H2ORegressionMetrics: gbm
## ** Reported on validation data. **
## ** Validation metrics **
##
## MSE: 2253.33
## RMSE: 47.46925
## MAE: 7.501601
## RMSLE: 0.1254609
## Mean Residual Deviance : 2253.33
##
##
## H2ORegressionMetrics: gbm
## ** Reported on cross-validation data. **
## ** 3-fold cross-validation on training data (Metrics computed for combined holdout predictions) **
##
## MSE: 1650.83
## RMSE: 40.6304
## MAE: 8.09533
## RMSLE: 0.1039192
## Mean Residual Deviance : 1650.83
##
##
## Cross-Validation Metrics Summary:
## mean sd cv_1_valid cv_2_valid
## mae 8.095536 1.005594 7.020234 8.253695
## mean_residual_deviance 1650.873700 842.582300 1420.309400 947.574650
## mse 1650.873700 842.582300 1420.309400 947.574650
## r2 0.977121 0.011315 0.979123 0.987301
## residual_deviance 1650.873700 842.582300 1420.309400 947.574650
## rmse 39.770000 10.189754 37.686993 30.782701
## rmsle 0.096042 0.048629 0.062442 0.073878
## cv_3_valid
## mae 9.012679
## mean_residual_deviance 2584.736800
## mse 2584.736800
## r2 0.964938
## residual_deviance 2584.736800
## rmse 50.840310
## rmsle 0.151804
Conclusiones del modelo baseline AutoML
Tras ejecutar el primer modelo base con H2O AutoML durante 5 minutos, se han entrenado un total de 284 modelos, siendo el mejor de ellos un modelo de tipo Gradient Boosting Machine (GBM). Este modelo alcanza un RMSE de 40.63 en validación cruzada y de 74.95 en la muestra de test, lo que constituye nuestro error base o baseline para comparaciones futuras.
El análisis de importancia de variables nos muestra una concentración muy clara en solo dos variables: square (superficie en metros cuadrados del inmueble) y price (precio por metro cuadrado). Ambas dominan la predicción con muchísima diferencia respecto al resto de variables.
Este resultado confirma que estas dos variables actúan como predictoras excesivas (hiperpredictoras), especialmente price, que está directamente relacionada con la variable objetivo totalPrice, lo que puede generar sobreajuste y resta valor interpretativo al modelo desde una perspectiva empresarial.
El resto de variables (como bathRoom, buildingType, fiveYearsProperty o floor) tienen un peso mínimo, lo cual puede estar condicionado por la fuerte influencia de square y price. En próximos pasos, eliminaremos estas variables y construiremos nuevas transformaciones (feature engineering) con el objetivo de mejorar la interpretabilidad y robustez del modelo.
Con el conocimiento adquirido tras la ejecución de nuestro primer modelo AutoML, en el que observamos una fuerte dependencia de las variables square y price, decidimos realizar una fase de ingeniería de variables para mejorar la explicabilidad y robustez del modelo.
En primer lugar, decidimos eliminar las variables price, square y DOM por su posible papel como variables hiperpredictoras o de escasa aportación predictiva independiente.
A continuación, construimos nuevas variables con el objetivo de capturar relaciones relevantes en el contexto de la predicción del precio total:
fe_dist: calculamos la distancia euclídea entre la localización de la vivienda (Lng, Lat) y un punto de referencia. Esta variable nos permite incorporar una medida de ubicación más refinada.
fe_tothabi: agregamos el número total de habitaciones (livingRoom, drawingRoom, kitchen, bathRoom) como un único indicador de tamaño funcional del piso.
fe_factors: unificamos todas las variables categóricas en un único string que resume la composición del piso. Esta transformación puede capturar interacciones complejas entre factores.
*Imputamos la variable buildingType en aquellos registros con valores NA, utilizando el resto de variables como referencia.
Con estas transformaciones, preparamos un nuevo dataset y volvemos a ejecutar un modelo de AutoML con H2O para evaluar el impacto de estos cambios.